﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Media;
using System.Text;
using System.Threading;

// use structure to store the data for the Sokoban hero
struct SokobanHero
{
    public int X;
    public int Y;
    public char C;
    public ConsoleColor Color;
}

// use structure to store the data for the Sokoban boxes (cubes)
struct Box
{
    public int x;
    public int y;
    public char ch;
    public ConsoleColor color;
    public bool canMove;

    public Box(int y, int x, char ch)
    {
        this.x = x;
        this.y = y;
        this.ch = ch;
        this.color = ConsoleColor.DarkGray;
        this.canMove = true;
    }
}

class SokobanGame
{
    //Constants/Options
    public const int winWidth = 80; //Console window width
    public const int winHeight = 40; //Console window height
    public const char levelBorder = '#'; //Border AND Obstacle
    public const char emptySpaceOutside = ' ';
    public const char emptySpaceInside = ' ';
    public const char heroDefSign = '@';
    public const char targetSign = 'X';
    public const char boxDefSign = '\u2588';
    public const char boxOnRightPositionSign = (char)2;

    //Variables
    public static char[,] currentBoard; //Board currently at play
    public static SokobanHero hero;
    public static List<Box> boxes; //List with all blocks that have to be moved
    public static int currentLevel = 3; //Level currently at play (INT)
    public static SoundPlayer backgroundMusic;
    public static int currentMoves = 0;//Moves made
    public static int boxesNotInRightPosCounter; //.....
    public static string userName = "Admin";
    public static IDictionary<string, int> playersByScore = new Dictionary<string, int>();
    public static string path = GetPath(currentLevel);
    private static string moveDirection = String.Empty; //Current direction name


    //Booleans
    public static bool mute = false; //Play or Mute sound;
    private static bool onCube = false;
    private static bool levelReset = false;
    private static bool quitLevel = false;
    private static bool showHighScore = false;
    private static bool mainMenu = true;
    private static bool showCredits = false;
    private static bool exitGame = false;
    public static bool gameCompleted = false;

    //Playlist
    public static string[] playlist = {
                                         "EMPTY ZERO INDEX",
                                        @"..\..\purple-hills-short.wav",
                                        @"..\..\house_impact.wav",
                                        @"..\..\technology.wav"
                                      };

    static void Main()
    {

        //Setting console borders
        Console.SetWindowSize(winWidth, winHeight);

        //Remove Scrolls
        Console.BufferWidth = Console.WindowWidth;
        Console.BufferHeight = Console.WindowHeight;

        //Hide cursor
        Console.CursorVisible = false;

        //Play music in the background - song provided by PlayOnLoop.com

        backgroundMusic = new SoundPlayer(playlist[2]);
        backgroundMusic.PlayLooping();

        //Print the Telerik Academy logo on startup
        PrintTelerikAcademyLogo();

        //Print game title
        PrintGameTitle();

        //Ask for name
        Console.Clear();
        PrintOnPosition(30, 10, "Enter nickname: ", ConsoleColor.Red);
        userName = Console.ReadLine();

        //Get the level from file. Can be selected from menu. Accepts INT 1-3
        GetLevel(currentLevel);

        //TO DO - STARTUP MENU
        DateTime start = DateTime.Now;// Time counter - used for the Infoboard

        while (true)
        {

            if (exitGame)
            {
                return;
            }

            if (mainMenu)
            {
                DrawInitialScreen();
                levelReset = true;
                currentLevel = 1;
                // mainMenu = false;
            }

            //Clear the console
            Console.Clear();

            //If level is cleared.
            if (boxesNotInRightPosCounter == 0)
            {
                PrintOnPosition(2, 0, "LEVEL COMPLETED", ConsoleColor.Red);

                HightScore();

                currentLevel++;

                Thread.Sleep(5000);

                currentMoves = 0;

                start = DateTime.Now;

                if (currentLevel >= 4)
                {
                    gameCompleted = true;
                }
                else
                {
                    GetLevel(currentLevel);
                }

            }

            if (gameCompleted)
            {
                Console.Clear();
                ShowCredits();
                Thread.Sleep(8000);
                mainMenu = true;
            }

            //On level reset
            if (levelReset)
            {
                currentMoves = 0;
                start = DateTime.Now;
                GetLevel(currentLevel);
                levelReset = false;
                continue;
            }

            //On show high score
            if (showHighScore)
            {
                HightScore();
                Thread.Sleep(3000);
                showHighScore = false;//Return to game
                continue;
            }

            if (showCredits)
            {
                showCredits = false;
                ShowCredits();
                Thread.Sleep(3000);
            }

            //On level quit
            if (quitLevel)
            {
                mainMenu = true;
                quitLevel = false;
                continue;
            }
            //Print current level
            PrintLevel();

            //PRINT INGAME MENU
            Infoboard(currentMoves, userName, start);

            //Listen for action
            ListenForAction();

            //Slow down game execution
            Thread.Sleep(90);
        }
    }

    private static void ListenForAction()
    {
        if (Console.KeyAvailable)
        {
            ConsoleKeyInfo pressedKey = Console.ReadKey(true);

            while (Console.KeyAvailable)
            {
                Console.ReadKey(true); //The sokoban moves without lagging
            }

            if (pressedKey.Key == ConsoleKey.LeftArrow)
            {
                moveDirection = "left";
                MoveHero(-1, 0);
            }
            else if (pressedKey.Key == ConsoleKey.RightArrow)
            {
                moveDirection = "right";
                MoveHero(1, 0);
            }
            else if (pressedKey.Key == ConsoleKey.UpArrow)
            {
                moveDirection = "up";
                MoveHero(0, -1);
            }
            else if (pressedKey.Key == ConsoleKey.DownArrow)
            {
                moveDirection = "down";
                MoveHero(0, 1);
            }
            else if (pressedKey.Key == ConsoleKey.R)
            {
                levelReset = true;
            }
            else if (pressedKey.Key == ConsoleKey.Q)
            {
                quitLevel = true;
            }
            else if (pressedKey.Key == ConsoleKey.H)
            {
                showHighScore = true;
            }
            else if (pressedKey.Key == ConsoleKey.N)
            {
                mainMenu = false;
            }
            else if (pressedKey.Key == ConsoleKey.M)
            {
                backgroundMusic.Stop();
                mute = true;
            }
            else if (pressedKey.Key == ConsoleKey.S)
            {
                backgroundMusic.PlayLooping();
                mute = false;
            }
            else if (pressedKey.Key == ConsoleKey.C)
            {
                showCredits = true;
            }
            else if (pressedKey.Key == ConsoleKey.E)
            {
                exitGame = true;
            }
            else if (pressedKey.Key == ConsoleKey.D1 ||
                     pressedKey.Key == ConsoleKey.D2 ||
                     pressedKey.Key == ConsoleKey.D3 )
            {
                backgroundMusic.Stop();
                backgroundMusic.SoundLocation = playlist[int.Parse(pressedKey.KeyChar.ToString())];
                backgroundMusic.PlayLooping();
            }
        }
    }


    private static void MoveHero(int xChange, int yChange)
    {
        int newX = hero.X + xChange;
        int newY = hero.Y + yChange;
        char targetChar = currentBoard[newY, newX];

        if (IsHeroMovable(newY, newX))
        {
            if (targetChar == targetSign && hero.Color != ConsoleColor.Blue)
            {
                hero.Color = ConsoleColor.Blue;
                currentBoard[hero.Y, hero.X] = emptySpaceInside;
            }
            else if (targetChar == targetSign && hero.Color == ConsoleColor.Blue)
            {
                hero.Color = ConsoleColor.Blue;
                currentBoard[hero.Y, hero.X] = targetSign;
            }
            else if (hero.Color == ConsoleColor.Blue)
            {
                hero.Color = ConsoleColor.Green;
                currentBoard[hero.Y, hero.X] = targetSign;
            }
            else
            {
                currentBoard[hero.Y, hero.X] = emptySpaceInside;
                if (targetChar == boxOnRightPositionSign)
                {
                    onCube = true;
                }
                else if (onCube)
                {
                    onCube = false;
                    hero.Color = ConsoleColor.Green;
                    currentBoard[hero.Y, hero.X] = targetSign;
                }
            }
            currentBoard[newY, newX] = hero.C;
            hero.X = newX;
            hero.Y = newY;
            currentMoves++;
        }
    }

    private static bool IsHeroMovable(int y, int x)
    {
        char destination = currentBoard[y, x];

        if (destination == levelBorder)
        {
            return false;
        }
        else if (destination == boxDefSign)
        {
            return MoveCurrentBox(y, x, boxDefSign);
        }
        else if (destination == boxOnRightPositionSign)
        {
            return MoveCurrentBox(y, x, boxOnRightPositionSign);
        }
        else
        {
            return true;
        }
    }

    public static bool MoveCurrentBox(int y, int x, char ch)
    {

        Box currBox = boxes.Find(b => (b.x == x && b.y == y && b.ch == ch));

        if (!currBox.canMove)
        {
            return false;
        }

        int currXPos = currBox.x;
        int currYPos = currBox.y;

        if (moveDirection == "up")
        {
            currYPos--;
        }
        else if (moveDirection == "down")
        {
            currYPos++;
        }
        else if (moveDirection == "left")
        {
            currXPos--;
        }
        else if (moveDirection == "right")
        {
            currXPos++;
        }


        if (currentBoard[currYPos, currXPos] != levelBorder &&
            currentBoard[currYPos, currXPos] != boxOnRightPositionSign &&
            currentBoard[currYPos, currXPos] != boxDefSign)
        {
            boxes.Remove(currBox);
            if (currentBoard[currYPos, currXPos] == emptySpaceInside)
            {

                if (currBox.ch == boxOnRightPositionSign)
                {
                    currentBoard[currBox.y, currBox.x] = targetSign;
                    currBox.ch = boxDefSign;
                    boxesNotInRightPosCounter++;

                }
                else
                {
                    currentBoard[currBox.y, currBox.x] = emptySpaceInside;
                }

            }
            else if (currentBoard[currYPos, currXPos] == targetSign)
            {
                if (currBox.ch == boxOnRightPositionSign)
                {
                    currentBoard[currBox.y, currBox.x] = targetSign;
                    boxesNotInRightPosCounter++;

                }
                else
                {
                    currBox.ch = boxOnRightPositionSign;
                    currentBoard[currBox.y, currBox.x] = emptySpaceInside;
                }

                boxesNotInRightPosCounter--;
            }

            currBox.x = currXPos;
            currBox.y = currYPos;
            currBox.canMove = IsBoxMovable(currYPos, currXPos);
            currentBoard[currBox.y, currBox.x] = currBox.ch;
            boxes.Add(currBox);

            return true;
        }

        return false;
    }

    private static bool IsBoxMovable(int y, int x)
    {
        if (y + 1 < currentBoard.GetLength(0) && (currentBoard[y + 1, x] == emptySpaceInside ||
            currentBoard[y + 1, x] == targetSign))
        {
            return true;
        }

        if (y - 1 >= 0 && (currentBoard[y - 1, x] == emptySpaceInside || currentBoard[y - 1, x] == targetSign))
        {
            return true;
        }

        if (x + 1 < currentBoard.GetLength(1) && (currentBoard[y, x + 1] == emptySpaceInside ||
                            currentBoard[y, x + 1] == targetSign))
        {
            return true;
        }

        if (x - 1 >= 0 && (currentBoard[y, x - 1] == emptySpaceInside || currentBoard[y, x - 1] == targetSign))
        {
            return true;
        }

        return false;
    }

    static void PrintOnPosition(int x, int y, char c, ConsoleColor color = ConsoleColor.Red)
    {
        Console.SetCursorPosition(x, y);
        Console.ForegroundColor = color;
        Console.Write(c);
    }

    //Overload for string
    static void PrintOnPosition(int x, int y, string c, ConsoleColor color = ConsoleColor.White)
    {
        Console.SetCursorPosition(x, y);
        Console.ForegroundColor = color;
        Console.Write(c);
    }

    private static void PrintGameTitle()
    {
        StreamReader gameLogo = new StreamReader(@"..\..\SokobanLogo.txt");

        using (gameLogo)
        {
            string line = gameLogo.ReadLine();
            int lineNum = 0;

            while (line != null)
            {
                Console.SetCursorPosition(25, lineNum);
                Console.ForegroundColor = ConsoleColor.Green;

                // get the logo slowly printed on the console
                Thread.Sleep(100);
                Console.WriteLine(line);

                line = gameLogo.ReadLine();
                lineNum++;
            }

            Console.ForegroundColor = ConsoleColor.Red;
            Console.SetCursorPosition(32, lineNum+2);
            Console.WriteLine("S O K O B A N");

            Console.SetCursorPosition(33, lineNum + 3);
            Console.WriteLine("written by");

            Console.SetCursorPosition(25, lineNum + 4);
            Console.WriteLine("T E A M   \"D A R E D E V I L\"");
        }


        // pause the logo on the screen
        Thread.Sleep(3000);

        // clean the console and reset its settings to begin the game
        Console.Clear();
        Console.ForegroundColor = ConsoleColor.White;
    }

    private static void PrintTelerikAcademyLogo()
    {
        StreamReader logoTelerikAcademy = new StreamReader(@"..\..\ninja.txt");

        using (logoTelerikAcademy)
        {
            string line = logoTelerikAcademy.ReadLine();
            int lineNum = 0;

            while (line != null)
            {
                Console.SetCursorPosition(15, lineNum);
                Console.ForegroundColor = ConsoleColor.Green;

                // get the logo slowly printed on the console
                Thread.Sleep(100);
                Console.WriteLine(line);

                line = logoTelerikAcademy.ReadLine();
                lineNum++;
            }

            Console.SetCursorPosition(30, lineNum);
            Console.WriteLine("Telerik Academy");

            Console.SetCursorPosition(34, lineNum + 1);
            Console.WriteLine("Teamwork");

            Console.SetCursorPosition(33, lineNum + 2);
            Console.WriteLine("C# - Part 2");
        }

        // pause the logo on the screen
        Thread.Sleep(3000);

        // clean the console and reset its settings to begin the game
        Console.Clear();
        Console.ForegroundColor = ConsoleColor.White;
    }

    //Parses the given level from file. 
    public static void GetLevel(int levelNum = 1)
    {
        currentLevel = levelNum;

        StreamReader getLevel = new StreamReader(@"..\..\level" + levelNum + ".txt");

        using (getLevel)
        {
            var size = getLevel.ReadLine().Split();
            currentBoard = new char[int.Parse(size[0]), int.Parse(size[1])];
            boxes = new List<Box>();

            for (int y = 0; y < currentBoard.GetLength(0); y++)
            {
                var line = getLevel.ReadLine().ToCharArray();

                for (int x = 0; x < currentBoard.GetLength(1); x++)
                {
                    char currentChar = line[x];

                    if (currentChar == 'S') //Hero starting position
                    {
                        hero = new SokobanHero();
                        hero.X = x;
                        hero.Y = y;
                        hero.C = heroDefSign;
                        hero.Color = ConsoleColor.Green;
                        currentChar = heroDefSign;
                    }
                    else if (currentChar == '#') //Border or obstacle
                    {
                        currentChar = levelBorder;
                    }
                    else if (currentChar == '0') //Empty space inside the level
                    {
                        currentChar = emptySpaceInside;
                    }
                    else if (currentChar == '-') //Empty space outside...
                    {
                        currentChar = emptySpaceOutside;
                    }
                    else if (currentChar == 'X') //Target for the boxes
                    {
                        currentChar = targetSign;
                    }
                    else if (currentChar == 'K') //Box/Kube/Cube :)
                    {
                        currentChar = boxDefSign;
                        Box box = new Box(y, x, currentChar);
                        boxes.Add(box);
                    }

                    currentBoard[y, x] = currentChar;
                }
            }

            boxesNotInRightPosCounter = boxes.Count;
        }
    }

    //Prints the current level to the console.
    static void PrintLevel()
    {
        for (int y = 0; y < currentBoard.GetLength(0); y++)
        {
            for (int x = 0; x < currentBoard.GetLength(1); x++)
            {
                ConsoleColor color = ConsoleColor.Red;
                if (currentBoard[y, x] == hero.C)
                {
                    color = hero.Color;
                }
                else if (currentBoard[y, x] == targetSign)
                {
                    color = ConsoleColor.White;
                }
                else if (currentBoard[y, x] == boxDefSign)
                {
                    color = ConsoleColor.Yellow;
                }
                else if (currentBoard[y, x] == boxOnRightPositionSign)
                {
                    color = ConsoleColor.White;
                }
                try
                {
                    PrintOnPosition(x + 1, y + 1, currentBoard[y, x], color);
                }
                catch (IndexOutOfRangeException)
                {
                    mainMenu = true;
                }
            }

            Console.WriteLine();
        }
    }

    //Hight score list.
    static string GetPath(int currentLevel)
    {
        switch (currentLevel)
        {
            case 1: path = @"..\..\High Scores Level 1.txt"; break;
            case 2: path = @"..\..\High Scores Level 2.txt"; break;
            case 3: path = @"..\..\High Scores Level 3.txt"; break;
        }

        return path;
    }

    static IDictionary<string, int> FillDictionary(string currentPathIs)
    {
        using (StreamReader reader = new StreamReader(path))
        {
            string line = reader.ReadLine();

            while (line != null)
            {
                string[] keyAndValue = line.Split(' ');
                playersByScore.Add(keyAndValue[0], int.Parse(keyAndValue[1]));
                line = reader.ReadLine();
            }
        }

        if (boxesNotInRightPosCounter == 0)
        {
            if (playersByScore.ContainsKey(userName))
            {
                playersByScore[userName] = currentMoves;
            }
            else
            {
                playersByScore.Add(userName, currentMoves);
            }
        }

        return playersByScore;
    }

    static void SortAndRewrite()
    {
        Console.WriteLine();
        Console.WriteLine("{0, 17}", "HIGH SCORE LIST");
        Console.WriteLine("{0, 18}", new string('*', 17));

        using (StreamWriter writer = new StreamWriter(path, false))
        {
            foreach (var score in playersByScore.OrderBy(x => x.Value))
            {
                writer.WriteLine("{0} {1}", score.Key, score.Value);
                Console.WriteLine("{0,10} - {1, 4}", score.Key, score.Value);
            }
        }

        Console.WriteLine("{0, 18}", new string('*', 17));
        playersByScore.Clear();
    }

    static void HightScore()
    {
        try
        {
            string currentPath = GetPath(currentLevel);
            FillDictionary(currentPath);
            SortAndRewrite();
        }
        catch (FileNotFoundException)
        {
            Console.WriteLine(@"Error: The file {0} containing the score list
       was not found in the program's directory. 
       Please, search for the file in other directories 
       or create a new one in it's place.", path);
        }

    }

    //Prints on the right of the field the player's name, time elapsed, moves made so far and menu commands.
    static void Infoboard(int moves, string name, DateTime start)
    {
        PrintOnPosition(currentBoard.GetLength(0) + 10, 1, "Player: " + name, ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 2, "Moves: " + moves, ConsoleColor.Cyan);
        TimeSpan time = (DateTime.Now - start);
        Console.SetCursorPosition(currentBoard.GetLength(0) + 10, 3);
        Console.WriteLine("Time Elapsed: {0:mm\\:ss}", time);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 6, "<R> Reset Level", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 7, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 8, "<Q> Quit Game", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 9, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 10, (mute ? "<S> Turn ON Music" :
            "<M> Mute Music"), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 11, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 12, "Playlist", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 13, "<1><2><3>", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 14, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 15, "<H> Show Hughscore", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 16, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 17, "<C> Show Credits", ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 18, new string('-', 15), ConsoleColor.Cyan);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 19, "<E> Exit Game", ConsoleColor.Cyan);

        PrintOnPosition(0, 21, "Some unknown ninjas ate too much shkembe after not drinking a lot of beer,", ConsoleColor.Cyan);
        PrintOnPosition(0, 22, "they felt to do some pranks and decided to exchange box places. Fortunately, ", ConsoleColor.Cyan);
        PrintOnPosition(0, 23, "they marked the original places with X. The poor sokoban, known as Warehouse ", ConsoleColor.Cyan);
        PrintOnPosition(0, 24, "keeper, also called Bai Ivan, has to put the boxes on their places. ", ConsoleColor.Cyan);
        PrintOnPosition(0, 25, "Unfortunately, he drank a lot of NotRakia and he seems to be confused. So, being", ConsoleColor.Cyan);
        PrintOnPosition(0, 26, "a top developer you can give him some hints. And because Bai Ivan is gotin dude", ConsoleColor.Cyan);
        PrintOnPosition(0, 27, "he promises to share the box content with you. He was told the boxes were for the", ConsoleColor.Cyan);
        PrintOnPosition(0, 28, "AfterParty after C# - Part 2 Exam @ TelerikAcademy. Moreover, he knows that some", ConsoleColor.Cyan);
        PrintOnPosition(0, 29, "of the boxes are full of blaginki like beer and Baio-Zaio. So, would you help him?", ConsoleColor.Cyan);
    }

    static void ShowCredits()
    {
        PrintOnPosition(currentBoard.GetLength(0) + 10, 1, "TEAM DAREDEVIL", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 2, "", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 3, "Adriana Dimanova (ADimanova)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 4, "Dimitar Kostov (d.kostov88)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 5, "Dimitar Mitev (mita4itu)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 6, "Martin Hristov (MartinHristov)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 7, "Stoyan Berov (stoberov)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 8, "Valentin Peltekov (valiyo)", ConsoleColor.Red);
        PrintOnPosition(currentBoard.GetLength(0) + 10, 9, "Viktoria Lyomcheva (viktoria_lyomcheva )", ConsoleColor.Red);
    }

    private static void DrawInitialScreen()
    {
        // Background
        Console.Clear();

        StreamReader introMenu = new StreamReader(@"..\..\introMenu.txt");

        using (introMenu)
        {
            string line = introMenu.ReadLine();
            int lineNum = 0;

            while (line != null)
            {
                Console.SetCursorPosition(3, lineNum);
                Console.ForegroundColor = ConsoleColor.Green;

                // get the menu slowly printed on the console
                Thread.Sleep(10);
                Console.WriteLine(line);

                line = introMenu.ReadLine();
                lineNum++;
            }

            ConsoleKeyInfo pressedKey = Console.ReadKey(true);

            if (pressedKey.Key == ConsoleKey.E)
            {
                exitGame = true;
            }
            else if (pressedKey.Key == ConsoleKey.N)
            {
                mainMenu = false;
            }
        }
    }
}